#!/usr/bin/env python
#
# Aggregator for generating grid-wide results as site-specific results are
# observed from grid components
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

'''
Aggregator for generating grid-wide results as site-specific results are
observed from grid components. The Aggregator class is an inline component
for processing results on the fly, passing the original results through to
another output handler. Aggregation results are sent to the same output
handler.

Aggregation works by recording the values of all results that have non-empty
delta_fields dictionaries. Only a subset of results support aggregation. See
the results module documentation for details.
'''

from kernel import Output
from results import AggregationResult


class Aggregator(Output):
   '''
   Aggregation output handler: aggregates some types of results, creating
   AggregationResult objects that are sent to the downstream output handler.
   Instances of this class are designed to be used as intermediate processing
   steps in the output pipeline, not as final output destinations.
   '''
   def __init__(self, output_handler):
      '''
      @param output_handler: Downstream output pipeline component (required)
      '''
      Output.__init__(self, output_handler)
      self.aggregated_data = {}    # result_type: {field: value}
      self.last_result = {}   # result_entity: {field: value}
      self.timestamps = {}    # result_type: timestamp
      self.relative_times = {}  # result_type: relative time
      self.first_timestamps = {}  # result_type: timestamp
   #
   def __append_aggregate(self, agg_type):
      '''
      Private method: creates an AggregationResult and sends this new result
      to the downstream output handler

      @param agg_type: Aggregation name
      @type agg_type: str
      '''
      data = self.aggregated_data[agg_type]
      xtime = self.timestamps[agg_type]
      ntime = self.relative_times[agg_type]
      r = AggregationResult(xtime, ntime, agg_type, data)
      self.output_handler.append(r)
   #
   def __delta(self, field, new_result):
      '''
      Private method: computes the difference in a newly observed aggregation
      value from the saved prior value for the same source entity (or zero if
      no prior value has been saved).

      @param field: Aggregation field name
      @type field: str
      @param new_result: Newly observed result object

      @rtype: number
      @return: the difference between the new and old values
      '''
      entity = new_result.entity
      rhs = 0
      if entity in self.last_result:
         if field in self.last_result[entity]:
            rhs = self.last_result[entity][field]
      lhs = new_result.delta_fields[field]
      return (lhs - rhs)
   #
   def __add_result(self, result):
      '''
      Private method: adds a result to the aggregation data. The result must
      have a non-empty delta_fields member, which should be checked before
      calling this method.

      @param result: Aggregatable result to add to the data set
      '''
      rtype = result.result_type
      entity = result.entity
      if rtype not in self.aggregated_data:
         self.aggregated_data[rtype] = result.delta_fields.copy()
         self.timestamps[rtype] = result.time
         self.relative_times[rtype] = 0
         self.first_timestamps[rtype] = result.time
      else:
         if result.time > self.timestamps[rtype]:
            self.__append_aggregate(rtype)
            self.timestamps[rtype] = result.time
            rel_time = result.time - self.first_timestamps[rtype]
            self.relative_times[rtype] = rel_time
         #
         for field in result.delta_fields:
            if field not in self.aggregated_data[rtype]:
               self.aggregated_data[rtype][field] = 0
            self.aggregated_data[rtype][field] += self.__delta(field, result)
      self.last_result[entity] = result.delta_fields
   #
   def handle_append(self, result):
      '''
      Receives a new result and handles the aggregation function. This method
      is invoked by the append() method of the parent class.

      @param result: Result to be received
      '''
      if len(result.delta_fields) > 0:
         self.__add_result(result)
   #
   def handle_close(self):
      '''
      Method invoked by parent: flushes the final aggregation to create final
      results at the end of a simulation
      '''
      for agg_type in self.aggregated_data:
         self.__append_aggregate(agg_type)
#
